Chomu's Blog.

>

Posts

GitHub

서버 컴포넌트에서 쿠키 등록 받기

목차

사전 지식

서버 컴포넌트

Next.js 는 컴포넌트가 어디서 렌더링 되는지에 따라 2가지의 종류로 나뉜다.
서버에서 렌더링(SSR)되어 보내지는 컴포넌트는 서버 컴포넌트,
클라이언트에서 코드를 받아 렌더링(CSR)되는 컴포넌트는 클라이언트 컴포넌트라고 한다.
Next.js 13 기준 클라이언트 컴포넌트를 만들기 위해서는 컴포넌트 코드 파일 상단에 "use client" 라고 선언해야 한다.
그외의 경우에는 모두 기본적으로 서버 컴포넌트로 렌더링 된다.

쿠키 등록하기

Next.js 에서 쿠키를 사용하기 위해서는 next/headerscookies 를 사용한다.
이 때, 단순 조회는 서버 컴포넌트에서도 가능하다.
하지만 쿠키를 등록하는 과정은 서버 액션 혹은 라우트 핸들러에서만 가능하다.
서버 액션은 서버 측에서 실행되는 함수이고, 라우트 핸들러는 API 라우트에서 사용하는 함수이다.

해결할 과제

목표

access, refresh 토큰을 쿠키에 저장해서 사용하는 방식으로 로그인을 구현하고 있었다.
로그인이 되어있는지 확인하는 과정에서, access 토큰이 만료되었다면 refresh 토큰을 사용해서 새로운 access 토큰을 발급받고 싶었다.
이를 위해 다음과 같은 코드를 작성했다.

// utils/getAccess.ts
"use server";
 
import { cookies } from "next/headers";
 
export default async function getAccess(): Promise<string> {
  // access 토큰이 있으면 그대로 반환
  const access = cookies().get("access")?.value;
  if (access) return access;
  // 없다면 먼저 refresh 토큰을 조회
  const refresh = cookies().get("refresh")?.value;
  if (!refresh) // 없다면 실패 처리;
  try {
    // refresh 토큰을 사용해서 새로운 access 토큰을 발급하고 저장 후 반환
    const access = await requestAccess(refresh);
    cookies().set("access", access);
    return access;
  } catch (e) {
    // 에러 시 실패 처리
  }
}

그리고 로그인이 필요한 페이지에 다음과 같은 코드를 작성했다.

// app/login-required-page.tsx
import { redirect } from "next/navigate";
import getAccess from "@/utils/getAccess";
 
export default async function LoginRequiredPage(){
  if (await getAccess()) redirect("/login");
  ...
}

하지만 getAccess 가 쿠키를 등록하는 과정에서 에러가 발생했다.
해당 페이지는 서버 컴포넌트이기 때문에 쿠키를 등록할 수 없다는 에러였다.

해결 과정

1. 클라이언트에서 API에 요청 보내기

먼저 로그인을 확인하는 API를 만들어서 해결해 보려고 했다.

// app/api/login/route.ts
import getAccess from "@/utils/getAccess";
 
export async function GET(){
  const access = await getAccess();
  if (!access) {
    // 로그인 실패 처리 (로그인 페이지 리다이렉트 등)
  }
  // 로그인 성공 처리 (쿠키 재등록 등)
}

이를 위해서는 클라이언트에서 서버로 다시 요청을 보내야 했다.
즉, 로그인이 필요한 모든 페이지를 클라이언트에서 처리하는 클라이언트 컴포넌트로 만들어야 했다.
Next.js의 SSR을 적극적으로 사용해보고 싶었기 때문에 이 방법은 사용하지 않았다.

2. 클라이언트를 API로 넘기기

다음으로 생각한 방법은 아예 클라이언트를 API로 보내버리는 것이었다.
쿠키 조회는 제한이 없으니 먼저 페이지에서 access 쿠키가 존재하는 지만 확인한다.
쿠키가 없다면 페이지의 경로를 URL 파라미터에 담아 API로 리다이렉트 한다.

// app/login-required-page.tsx
import { redirect } from "next/navigate";
import { cookies } from "next/headers";
 
export default async function LoginRequiredPage(){
  if (cookies().has("access")) redirect("/api/login?redirect=/login-required-page");
  ...
}

API에서는 다음과 같이 쿠키를 등록한다.

// app/api/login/route.ts
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
 
export async function GET(req: NextRequest) {
  const { origin } = req.nextUrl;
  const access = await getAccess();
  const redirect = access
    ? req.nextUrl.searchParams.get("redirect") || "/home"
    : "/login";
  return NextResponse.redirect(
    `${origin}${redirect}`,
    { status: 303 }
  );
}

개선할 점

클라이언트를 두 번이나 리다이렉트 하기 때문에 서버에 부하가 간다.
또한 사용자에게 순간적으로 빈 화면을 노출하기 때문에 UX 측면에서도 좋지 않다.
당장 생각나는 방법으로는 비어있는 클라이언트 컴포넌트를 만들어서 로그인이 되어있는지만 확인하는 방법도 괜찮을 것 같다는 생각이 든다.